2009/10/23

Recent entries from same category

  1. RapidJSON や simdjson よりも速いC言語から使えるJSONライブラリ「yyjson」
  2. コメントも扱える高機能な C++ 向け JSON パーサ「jsoncpp」
  3. C++ で flask ライクなウェブサーバ「clask」書いた。
  4. C++ 用 SQLite3 ORM 「sqlite_orm」が便利。
  5. zsh で PATH に相対パスを含んだ場合にコマンドが補完できないのは意図的かどうか。

githubが高速化に成功した様です。
How We Made GitHub Fast - GitHub
github

Now that things have settled down from the move to Rackspace, I wanted to take some time to go over the architectural changes that we’ve made in order to bring you a speedier, more scalable GitHub.

...

For our data serialization and RPC protocol we are using BERT and BERT-RPC.

http://github.com/blog/530-how-we-made-github-fast
データのシリアライズおよびRPC(リモートプロシージャコール)としてBERT-RPCを選んだ様です。
BERT and BERT-RPC 1.0 Specification

BERT and BERT-RPC are an attempt to specify a flexible binary serialization and RPC protocol that are compatible with the philosophies of dynamic languages such as Ruby, Python, PERL, JavaScript, Erlang, Lua, etc. BERT aims to be as simple as possible while maintaining support for the advanced data types we have come to know and love. BERT-RPC is designed to work seamlessly within a dynamic/agile development workflow. The BERT-RPC philosophy is to eliminate extraneous type checking, IDL specification, and code generation. This frees the developer to actually get things done.

http://bert-rpc.org/
BERT-RPCはBERT(Binary ERlang Term)という名前の通り、ERlangのterm_to_binary/1で生成されるフレキシブルなバイナリデータ交換の仕組みで、これをRPCに利用した物がBERT-RPCです。

BERT

BERT (Binary ERlang Term) is a flexible binary data interchange format based on (and compatible with) Erlang's binary serialization format (as used by erlang:term_to_binary/1).

http://bert-rpc.org/
erlangで使える、integer,float,atom,tupple,bytelist,list,binaryをシリアライズして転送する事が出来ます。仕様書はerlangの拡張仕様ページに掲載されています。
External Term Format

The external term format is mainly used in the distribution mechanism of Erlang.

http://erlang.org/doc/apps/erts/erl_ext_dist.html
詳しくはgithubのブログエントリでも説明されています。
最近ではMessagePackやProtocolBuffer、Thriftなどバイナリフォーマットを使ったRPCが流行ですが、このErlangのバイナリフォーマットは無駄の無いシリアライズ形式になっているかと思います。
実際には型識別と、それに後続するデータで構成され、型によってはデータ部の前にデータ長が付加されます。
Erlangのシリアライズでは、Erlangのprotocolバージョン131から開始されますが、その前に全体長を付与した物がRPCで使われています。RPCの本体はtuppleと、その中のATOMで構成されています。
実装を見たい方は以下のrubyによる実装を見る事が出来ます。
mojombo's bert at master - GitHub

BERT (Binary ERlang Term) serialization library for Ruby.

http://github.com/mojombo/bert

mojombo's bertrpc at master - GitHub

BERTRPC is a Ruby BERT-RPC client library.

http://github.com/mojombo/bertrpc
またerlangによるシリアライズ実装、およびBERT-RPCを使ったRPCサーバの実装は以下から参照出来ます。
mojombo's erlectricity at master - GitHub

Erlectricity exposes Ruby to Erlang and vice versa

http://github.com/mojombo/erlectricity

mojombo's ernie at master - GitHub

Ernie is an Erlang/Ruby BERT-RPC Server.

http://github.com/mojombo/ernie
例えば上記githubブログへのリンク先で説明されている様に # calc.rb
require 'ernie'

mod(:calc) do
  fun(:add) do |a, b|
    a + b
  end
end
というサーバのコードに対しては require 'bertrpc'

svc = BERTRPC::Service.new('localhost', 9999)
svc.call.calc.add(1, 2)
# => 3
というクライアントのコードが書けるのですが、内部ではErlangでシリアライズされ、先頭にデータ長が付与された以下のイメージでデータ送信が行われます。
{call,calc,add,[1,2]}
そしてバイト列としては以下の様になります。
0000000: 0000 0021 8368 0464 0004 6361 6c6c 6400  ...!.h.d..calld.
0000010: 0463 616c 6364 0003 6164 646c 0000 0002  .calcd..addl....
0000020: 6101 6102 6a                             a.a.j
先頭4バイトにデータ長(0x21:33バイト)、バージョン(0x83:131)、tupple(0x68)、tuppleアイテム数(4)、atom ext(0x64)...と続きます。

今日はこのBERT-RPCで動く足し算サーバををC言語から呼び出すサンプルを書いてみました。
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <stdio.h>
#include <string.h>
#include <fcntl.h>

// send BERT-RPC command
//
//   request => {call,calc,add,[1,2]}
//   result  => {reply,3}
int
main(int argc, char *argv[]) {
  int sock;
  struct sockaddr_in serv_addr;
  short nport = htons(9999);
  struct hostent *host_ent;
  char *host = (char*)strdup("127.0.0.1");
  char *ptr;
  FILE *sockfp;

  ptr = strchr(host, ':');
  if (ptr) {
    *ptr++ = 0;
    if (atoi(ptr) == 0) {
      struct servent *serv_ent;
      serv_ent = getservbyname(ptr, "tcp");
      if (serv_ent)
        nport = serv_ent->s_port;
    } else {
      nport = htons(atoi(ptr));
    }
  }
  ptr = host;
  if ((host_ent = gethostbyname(ptr)) == NULL) {
    sock = -1;
    return -1;
  }
  memset((char *)&serv_addr, 0, sizeof(serv_addr));
  serv_addr.sin_family = AF_INET;
  memcpy(&serv_addr.sin_addr, host_ent->h_addr, host_ent->h_length);
  serv_addr.sin_port = nport;

  sock = socket(AF_INET, SOCK_STREAM, 0);
  if (sock < 0) {
    return;
  }

  if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1) {
    close(sock);
    sock = -1;
    return;
  }

  char  n1;
  short n2;
  int   n4;

  // total length
  n4 = htonl(33);               // 4byte
  write(sock, &n4, sizeof(n4)); // number of bytes

  n1 = 131;
  write(sock, &n1, sizeof(n1)); // version 131

  // {call,module,function,arguments}
  // ex: {call,calc,add,[1,2]}
  n1 = 104;
  write(sock, &n1, sizeof(n1)); // tupple
  n1 = 4;
  write(sock, &n1, sizeof(n1)); // number of tupple items

    n1 = 100;
    write(sock, &n1, sizeof(n1)); // atom
    n2 = htons(4);
    write(sock, &n2, sizeof(n2)); // number of atom items

      // atom1
      write(sock, "call", 4);          // call

      // atom2
      n1 = 100;
      write(sock, &n1, sizeof(n1));    // atom ext
      n2 = htons(4);                   // 2byte
      write(sock, &n2, sizeof(n2));    // atom length
      write(sock, "calc", 4);          // atom name

      // atom3
      n1 = 100;
      write(sock, &n1, sizeof(n1));    // atom ext
      n2 = htons(3);                   // 2byte
      write(sock, &n2, sizeof(n2));    // number of atom items
      write(sock, "add", 3);           // atom name

      // atom4
      n1 = 108;
      write(sock, &n1, sizeof(n1));    // list extension
      n4 = htonl(2);                   // 4byte
      write(sock, &n4, sizeof(n4));    // number of list elements

        n1 = 97;
        write(sock, &n1, sizeof(n1));  // small int
        n1 = 1;
        write(sock, &n1, sizeof(n1));  // int 1

        n1 = 97;
        write(sock, &n1, sizeof(n1));  // small int
        n1 = 2;
        write(sock, &n1, sizeof(n1));  // int 2

      n1 = 106;
      write(sock, &n1, 1); // nil ext(term of list extension)

  char buf[256];
  ptr = buf;
  int x = 0, n, l;
  l = recv(sock, buf, sizeof(buf), 0);
  printf("recv size %d\n", l);
  for (n = 0; n < l; n++) {
      x = (x + 1) % 80;
      printf("%02X ", (unsigned char)buf[n]);
      if (x == 0) putchar('\n');
  }
  putchar('\n');

  // [ 13bytes ] []
  // 00 00 00 0D 83 68 02 64 00 05 72 65 70 6C 79 61 03

  // length
  n4 = (unsigned int) (
          (*(ptr+0) << 24) +
          (*(ptr+1) << 16) +
          (*(ptr+2) <<  8) +
          (*(ptr+3) <<  0));
  printf("total length = %d\n", n4);
  ptr+=4;

  // version
  n1 = *ptr;
  printf("version = %d\n", (unsigned char)n1);
  ptr++;

  // small tupple extension
  n1 = *ptr;
  if (n1 == 104) printf("tupple extension\n");
  ptr++;

  // number of tupple items
  n1 = *ptr;
  printf("number of tupple items %d\n", n1);
  ptr++;

  // atom ext
  n1 = *ptr;
  if (n1 == 100) printf("atom extension\n");
  ptr++;

  // atom length
  n2 = (unsigned short) (
          (*(ptr+0) <<  8) +
          (*(ptr+1) <<  0));
  printf("atom length = %d\n", n2);
  ptr+=2;

  // atom name
  char atom[200] = {0};
  memcpy(atom, ptr, n2);
  printf("atom name = %s\n", atom);
  ptr+=n2;

  // small int
  n1 = *ptr;
  if (n1 == 97)printf("small int\n");
  ptr++;
  n1 = *ptr;
  printf("int %d\n", n1);

  close(sock);
  sock = -1;
  return 0;
}
  
  

分かりやすい様にコメントに種別を書いていますが、実用するには構造化したり抽象化する必要があります。ただし言語によっては型が見合わない場合もあるので、擬似するコードを書かなければならない可能性もあります。まだrubyとjavascriptにしか実装がない様なので、どなたかチャレンジしてみてはいかがでしょうか。
Posted at by